package com.coincalf.wallet.blockchain;

import com.alibaba.fastjson.JSON;
import com.coincalf.framework.enums.TxStateEnum;
import com.coincalf.wallet.blockchain.bean.TransInfo;
import com.coincalf.wallet.blockchain.client.UsdtClientSingleton;
import com.coincalf.wallet.blockchain.util.AESUtil;
import com.coincalf.wallet.blockchain.util.HttpUtil;
import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.core.*;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.script.Script;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wf.bitcoin.javabitcoindrpcclient.BitcoinJSONRPCClient;
import wf.bitcoin.javabitcoindrpcclient.BitcoindRpcClient;
import wf.bitcoin.javabitcoindrpcclient.GenericRpcException;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class BtcService {

    private static Logger logger = LoggerFactory.getLogger(BtcService.class);
    private static final double SATOSHI2BTC = 100000000D;

    private static final BigDecimal MIN_AMOUNT = new BigDecimal("0.00000546");

    /**
     * 获取btc client
     *
     * @return client
     */
    private static BitcoinJSONRPCClient getClient() {
        return UsdtClientSingleton.getUsdtClient();
    }

    /**
     * 获取BTC地址余额
     *
     * @param address 钱包地址
     * @return
     */
    public static BigDecimal getBalance(String address) {
        logger.info("获取比特币地址余额 address={}", address);
        try {
            List<UTXO> unspents = getUnspents(address);
            if (unspents == null) {
                return BigDecimal.ZERO;
            }
            long amount = 0L;
            for (UTXO utxo : unspents) {
                long value = utxo.getValue().value;
                amount += value;
            }
            BigDecimal value = BigDecimal.valueOf(amount);
            value = value.divide(BigDecimal.valueOf(SATOSHI2BTC), 8, RoundingMode.HALF_DOWN);
            return value;
        } catch (GenericRpcException e) {
            logger.error("获取比特币地址余额异常 address={} e={}", address, e);
            throw new BlockException(e.getMessage());
        }
    }

    /**
     * 构建BTC交易
     *
     * @param bean
     * @return
     */
    public static TransInfo trans(TransInfo bean) {
        logger.info("比特币交易入参={}", JSON.toJSONString(bean));
        TransInfo transInfo = new TransInfo();
        try {
            String prK = AESUtil.decrypt(bean.getPrivateKey(), BlockConstant.AES_KEY);
            long amount = bean.getAmount().multiply(BlockConstant.SATOSHIS).longValue();
            long fee = bean.getFee().multiply(BlockConstant.SATOSHIS).longValue();
            String txHash = offlineSignature(
                    bean.getFromAddress(),
                    bean.getToAddress(),
                    prK,
                    amount,
                    fee
            );
            //String txHash = btcRwaTx(bean.getFromAddress(), bean.getToAddress(), bean.getAmount());
            if (StringUtils.isBlank(txHash)) {
                transInfo.setStatus(TxStateEnum.FAIL.getStatus());
            } else {
                transInfo.setStatus(TxStateEnum.TO_BE_CONFIRMED.getStatus());
                transInfo.setTxHash(txHash);
                transInfo.setAmount(bean.getAmount());
            }
        } catch (Exception e) {
            transInfo.setStatus(TxStateEnum.FAIL.getStatus());
            transInfo.setErrMsg(e.getMessage());
            logger.info("比特币交易失败 from={}  to={}  amount={}  e={}",
                    bean.getFromAddress(), bean.getToAddress(), bean.getAmount(), e
            );
        }
        return transInfo;
    }

    /**
     * 查询BTC交易详情
     *
     * @param txHash 交易Hash
     * @return
     */
    public static TransInfo query(String txHash) {
        TransInfo bean = new TransInfo();
        bean.setTxHash(txHash);
        try {
            BitcoinJSONRPCClient client = getClient();
            BitcoindRpcClient.RawTransaction transaction = client.getRawTransaction(txHash);
            logger.info(">>>>>> {} <<<<<<<", transaction);
            bean.setConfirmations((long) transaction.confirmations());
            BigDecimal txOut = BigDecimal.ZERO;
            BigDecimal txIn = BigDecimal.ZERO;
            List<BitcoindRpcClient.RawTransaction.Out> outs = transaction.vOut();
            //交易输出
            for (BitcoindRpcClient.RawTransaction.Out out : outs) {
                txOut = txOut.add(out.value());
            }
            //交易输入
            List<BitcoindRpcClient.RawTransaction.In> ins = transaction.vIn();
            for (BitcoindRpcClient.RawTransaction.In in : ins) {
                BitcoindRpcClient.RawTransaction.Out transactionOutput = in.getTransactionOutput();
                txIn = txIn.add(transactionOutput.value());
            }
            //交易费
            BigDecimal fee = txIn.subtract(txOut);
            bean.setFee(fee.setScale(8, BigDecimal.ROUND_HALF_UP));
            if (transaction.blockHash() == null) {
                bean.setStatus(TxStateEnum.TO_BE_CONFIRMED.getStatus());
                logger.info("交易txHash={} 未同步", txHash);
                return bean;
            }
            bean.setBlockNumber(transaction.height());
            bean.setStatus(TxStateEnum.SUCCESS.getStatus());
        } catch (Exception e) {
            bean.setStatus(TxStateEnum.TO_BE_CONFIRMED.getStatus());
            logger.error("查询比特币交易详情异常 txHash={}  e={}", txHash, e);
        }
        return bean;
    }

    /**
     * 导入BTC私钥
     *
     * @param privateKey 私钥
     * @param account    地址(userId)
     */
    public void importPrivateKey(String privateKey, String account) {
        logger.info("btc importPrivateKey account={}", account);
        try {
            BitcoinJSONRPCClient client = getClient();
            client.importPrivKey(privateKey, account, false);
        } catch (Exception e) {
            logger.error("导入比特币私钥异常 account={} e={}", account, e);
            e.printStackTrace();
        }
    }

    /**
     * 导出BTC私钥
     *
     * @param address  钱包地址
     * @param password 密码 不需要密码
     * @return
     */
    public String exportPrivateKey(String address, String password) {
        logger.info("导出比特币私钥start address={}", address);
        try {
            BitcoinJSONRPCClient client = getClient();
            //             client.walletPassPhrase("pwd", 60L); 未设置密码 则不要解密
            return client.dumpPrivKey(address);
        } catch (Exception e) {
            logger.error("导出比特币私钥异常 account={} e={}", address, e);
        }
        return null;
    }

    public String btcRwaTx(String from, String to, BigDecimal amount) throws BlockException {
        BitcoinJSONRPCClient btcClient = getClient();
        List<BitcoindRpcClient.TxInput> txInputs = new ArrayList<>();
        List<BitcoindRpcClient.TxOutput> txOutputs = new ArrayList<>();
        BigDecimal satoshiAmount = amount.multiply(BlockConstant.SATOSHIS);
        //TODO 转账矿工费 直接设置两个交易输入 其实并不一定 再次计算
        //long feeL = (2 * 148 + 34 * 2 + 10) * UsdtBlockChainService.getFeeRate();
        //BigDecimal satoshiFee = BigDecimal.valueOf(feeL).multiply(SATOSHI2BTC2);
        //BigDecimal satoshiFee = BigDecimal.ZERO;
        //总金额
        long totalMoney = 0L;
        //遍历未花费交易输出列表，组装合适的item
        List<UTXO> unspentBTC = getUnspents(from);
        if (unspentBTC == null) {
            throw new BlockException("BTC INSUFFICIENT FEE");
        }
        //计算交易输入个数 本次计算不带矿工费的
        int inputSize = 0;
        for (UTXO unspentTxOut : unspentBTC) {
            if (totalMoney >= (satoshiAmount.longValue())) {
                break;
            }
            totalMoney += unspentTxOut.getValue().value;
            inputSize++;
        }
        inputSize++;//这里交易输入个数额外增加1 防止手续费过低导致交易一直不确认
        long feeL = (inputSize * 148 + 34 * 2 + 10) * UsdtService.getFeeRate();
        BigDecimal satoshiFee = BigDecimal.valueOf(feeL).multiply(BlockConstant.SATOSHIS);
        //加上手续费再次计算 其实还是会存在误差...
        totalMoney = 0L;
        for (UTXO unspentTxOut : unspentBTC) {
            if (totalMoney >= (satoshiAmount.add(satoshiFee).longValue())) {
                break;
            }
            //组装TxInput
            txInputs.add(new BitcoindRpcClient.BasicTxInput(
                    unspentTxOut.getHash().toString(),
                    (int) unspentTxOut.getIndex(),
                    unspentTxOut.getScript().toString()
            ));
            totalMoney += unspentTxOut.getValue().value;
        }
        //找零
        BigDecimal giveChange = BigDecimal.valueOf(totalMoney).subtract(satoshiAmount).subtract(satoshiFee);
        giveChange = giveChange.divide(BlockConstant.SATOSHIS, 8, RoundingMode.HALF_DOWN);
        logger.info("from={}, to={}, 找零金额={}", from, to, giveChange);
        if (giveChange.compareTo(BigDecimal.ZERO) < 0) {
            logger.info("BTC地址余额不足，转账失败  totalMoney={}, address={}", totalMoney, from);
            throw new BlockException("BTC INSUFFICIENT FEE");
        }
        if (giveChange.compareTo(MIN_AMOUNT) >= 0) {
            //找零地址
            txOutputs.add(new BitcoindRpcClient.BasicTxOutput(from, giveChange));
        } else {
            logger.info("找零费用太低，转为交易输出 from={}, to={}, amount={}, giveChange={}", from, to, amount, giveChange);
            amount = giveChange.add(amount);
        }
        txOutputs.add(new BitcoindRpcClient.BasicTxOutput(to, amount));
        String rawTransaction = btcClient.createRawTransaction(txInputs, txOutputs);
        String signRawTransaction = btcClient.signRawTransaction(rawTransaction);
        logger.info("BTC签名原始交易SUCCESS result={}", signRawTransaction);
        try {
            return btcClient.sendRawTransaction(signRawTransaction);
        } catch (GenericRpcException e) {
            if (e.getMessage().contains("dust")) {
                logger.error("BTC交易存在灰尘攻击交易 from={}, to={}, amount={}", from, to, amount);
                throw new BlockException("DUST TRAINS AMOUNT TOO LOW");
            } else {
                throw new BlockException(e.getMessage());
            }
        }
    }

    /**
     * BTC原始交易（要求必须是节点内的地址）
     * 一对一转账
     *
     * @param from   转账地址
     * @param to     收款地址
     * @param amount 转账金额 单位：BTC
     * @param fee    手续费 单位：BTC
     * @return
     * @throws RuntimeException
     */
    public String btcRwaTx(String from, String to, BigDecimal amount, BigDecimal fee) throws BlockException {
        BitcoinJSONRPCClient btcClient = getClient();
        List<BitcoindRpcClient.TxInput> txInputs = new ArrayList<>();
        List<BitcoindRpcClient.TxOutput> txOutputs = new ArrayList<>();
        BigDecimal satoshiAmount = amount.multiply(BlockConstant.SATOSHIS);
        BigDecimal satoshiFee = fee.multiply(BlockConstant.SATOSHIS);
        //总金额
        long totalMoney = 0L;
        //遍历未花费交易输出列表，组装合适的item
        List<UTXO> unspentBTC = getUnspents(from);
        if (unspentBTC == null) {
            throw new BlockException("BTC INSUFFICIENT FEE");
        }
        for (UTXO unspentTxOut : unspentBTC) {
            if (totalMoney >= (satoshiAmount.add(satoshiFee).longValue())) {
                break;
            }
            //组装TxInput
            txInputs.add(new BitcoindRpcClient.BasicTxInput(
                    unspentTxOut.getHash().toString(),
                    (int) unspentTxOut.getIndex(),
                    unspentTxOut.getScript().toString()
            ));
            totalMoney += unspentTxOut.getValue().value;
        }
        //找零
        BigDecimal giveChange = BigDecimal.valueOf(totalMoney).subtract(satoshiAmount).subtract(satoshiFee);
        giveChange = giveChange.divide(BlockConstant.SATOSHIS, 8, RoundingMode.HALF_DOWN);
        logger.info("from={}, to={}, 找零金额={}", from, to, giveChange);
        if (giveChange.compareTo(BigDecimal.ZERO) < 0) {
            logger.info("BTC地址余额不足，转账失败  totalMoney={}, address={}", totalMoney, from);
            throw new BlockException("BTC INSUFFICIENT FEE");
        }
        if (giveChange.compareTo(MIN_AMOUNT) >= 0) {
            //找零地址
            txOutputs.add(new BitcoindRpcClient.BasicTxOutput(from, giveChange));
        } else {
            logger.info("找零费用太低，转为交易输出 from={}, to={}, amount={}, giveChange={}", from, to, amount, giveChange);
            amount = giveChange.add(amount);
        }
        txOutputs.add(new BitcoindRpcClient.BasicTxOutput(to, amount));
        String rawTransaction = btcClient.createRawTransaction(txInputs, txOutputs);
        String signRawTransaction = btcClient.signRawTransaction(rawTransaction);
        logger.info("BTC签名原始交易SUCCESS result={}", signRawTransaction);
        try {
            return btcClient.sendRawTransaction(signRawTransaction);
        } catch (GenericRpcException e) {
            if (e.getMessage().contains("dust")) {
                logger.error("BTC交易存在灰尘攻击交易 from={}, to={}, amount={}", from, to, amount);
                throw new BlockException("DUST TRAINS AMOUNT TOO LOW");
            } else {
                throw new BlockException(e.getMessage());
            }
        }
    }

    /**
     * BTC离线签名交易 仅适用于1开头的地址
     * 一对一转账
     *
     * @param fromAddress 转账地址
     * @param toAddress   收款地址
     * @param privateKey  转账地址私钥
     * @param amount      转账数量 单位：聪
     * @param fee         转账手续费 单位：聪
     * @return
     * @throws BlockException
     */
    public static String offlineSignature(String fromAddress, String toAddress, String privateKey, long amount, long fee) throws BlockException {
        NetworkParameters networkParameters = MainNetParams.get();
        Transaction transaction = new Transaction(networkParameters);
        List<UTXO> utxos = getUnspents(fromAddress);
        if (utxos == null || utxos.size() == 0) {
            throw new BlockException("utxo为空 BTC INSUFFICIENT FEE");
        }
        long utxoAmount = 0L;
        List<UTXO> needUtxos = new ArrayList<UTXO>();
        //遍历未花费列表，组装合适的item
        for (UTXO utxo : utxos) {
            if (utxoAmount >= (amount + fee)) {
                break;
            } else {
                needUtxos.add(utxo);
                utxoAmount += utxo.getValue().value;
            }
        }
        //设置交易输出
        transaction.addOutput(Coin.valueOf(amount), Address.fromString(networkParameters, toAddress));
        //找零金额 = 消费列表总金额 - 转账的金额 - 手续费
        long changeAmount = utxoAmount - (amount + fee);
        if (changeAmount < 0) {
            throw new BlockException("utxo余额不足");
        }
        //找零 注意：BTC最小转账金额为546聪，如果找零金额小于546聪，则直接充当矿工费
        if (changeAmount >= 546) {
            transaction.addOutput(Coin.valueOf(changeAmount), Address.fromString(networkParameters, fromAddress));
        }
        //输入未消费列表项
        DumpedPrivateKey dumpedPrivateKey = DumpedPrivateKey.fromBase58(networkParameters, privateKey);
        ECKey ecKey = dumpedPrivateKey.getKey();
        for (UTXO utxo : needUtxos) {
            TransactionOutPoint outPoint = new TransactionOutPoint(networkParameters, utxo.getIndex(), utxo.getHash());
            transaction.addSignedInput(outPoint, utxo.getScript(), ecKey, Transaction.SigHash.ALL, true);
        }
        byte[] bytes = transaction.bitcoinSerialize();
        String signHex = Hex.toHexString(bytes);
        logger.info("BTC交易签名成功 signHex={}", signHex);
        try {
            return UsdtClientSingleton.getUsdtClient().sendRawTransaction(signHex);
        } catch (GenericRpcException e) {
            if (e.getMessage().contains("dust")) {
                logger.error("BTC交易存在灰尘攻击交易 from={}, to={}, amount={}", fromAddress, toAddress, amount);
                throw new BlockException("dust...");
            } else {
                throw new BlockException(e.getMessage());
            }
        }
    }

    /**
     * 获取Unspent Transaction Out
     *
     * @param address 地址
     * @return UTXO
     */
    private static List<UTXO> getUnspents(String address) {
        List<UTXO> utxos = new ArrayList<>();
        List<BitcoindRpcClient.Unspent> unspentOutPuts = getClient().listUnspent(6, 99999999, address);
        if (unspentOutPuts.size() == 0) {
            return null;
        }
        for (BitcoindRpcClient.Unspent unspent : unspentOutPuts) {
            String txId = unspent.txid();
            long vOut = unspent.vout();
            String script = unspent.scriptPubKey();
            String addressStr = unspent.address();
            BigDecimal amount = unspent.amount();
            amount = amount.multiply(BlockConstant.SATOSHIS);
            //int confirmations = unspent.confirmations();      //确认数 >= 6
            UTXO utxo = new UTXO(Sha256Hash.wrap(txId), vOut, Coin.valueOf(amount.longValue()),
                    0, false, new Script(Hex.decode(script)), addressStr
            );
            utxos.add(utxo);
        }
        return utxos;
    }

    /**
     * 获取矿工费用
     *
     * @param utxos
     * @return 当前最佳矿工费 单位：聪
     */
    public static Long getOmniFee(List<UTXO> utxos) {
        Long miniBtc = 546L;
        Long feeRate = getFeeRate();
        long utxoAmount = 0L;
        Long fee = 0L;
        long utxoSize = 0L;
        for (UTXO output : utxos) {
            utxoSize++;
            if (utxoAmount > (fee + miniBtc)) {
                break;
            } else {
                utxoAmount += output.getValue().value;
                //说明：utxoSize代表交易输入的个数  2代表交易输出的个数 10是固定值
                //可参考->https://www.chainnode.com/post/260288
                fee = (utxoSize * 148 + 34 * 2 + 10) * feeRate;
            }
        }
        return fee;
    }


    /**
     * 获取btc费率
     *
     * @return
     */
    public static Long getFeeRate() {
        try {
            String httpGet1 = HttpUtil.doGet("https://bitcoinfees.earn.com/api/v1/fees/recommended");
            Map map = JSON.parseObject(httpGet1, Map.class);
            Long fastestFee = Long.valueOf(map.get("fastestFee").toString());
            return fastestFee;
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 获取当前区块高度
     *
     * @return 区块高度
     */
    public static int getBlockCount() {
        return UsdtClientSingleton.getUsdtClient().getBlockCount();
    }

    /**
     * 根据区块高度获取区块hash
     *
     * @param height 区块高度
     * @return 区块哈希
     */
    public static String getBlockHash(int height) {
        return UsdtClientSingleton.getUsdtClient().getBlockHash(height);
    }

    /**
     * 根据交易hash获取交易详情
     *
     * @param txHash 交易哈希
     * @return 交易详情
     */
    public static BitcoindRpcClient.RawTransaction getRawTransaction(String txHash) {
        return UsdtClientSingleton.getUsdtClient().getRawTransaction(txHash);
    }

    /**
     * 获取区块详情
     *
     * @param blockHash 区块哈希
     * @return 区块详情
     */
    public static BitcoindRpcClient.Block getTxsHash(String blockHash) {
        return UsdtClientSingleton.getUsdtClient().getBlock(blockHash);
    }

}
